﻿using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.UI;
using Windows.UI.Xaml.Media.Imaging;

namespace TopoViewer
{
    public enum MapLevel : byte { L1, L2, L3, L4, L5 };

    class MapManager
    {
        // We do not do continuous scaling. Instead, we preset certain scale levels.
        // The first entity indicates the map level. The second is pixel/second scale.
        public static Tuple<MapLevel, double>[] PresetYScales =
        {
            Tuple.Create(MapLevel.L3, -960.0/3600),     // 0.267, native scale of L3 maps
            Tuple.Create(MapLevel.L4, -2495.0/1800),    // 1.747
            Tuple.Create(MapLevel.L4, -3144.0/1800),    // 1.747, native scale of L4 maps
            Tuple.Create(MapLevel.L4, -3961.0/1800),    // 2.201
            Tuple.Create(MapLevel.L5, -1280.0/450),     // 2.844
            Tuple.Create(MapLevel.L5, -1613.0/450),     // 3.584
            Tuple.Create(MapLevel.L5, -2032.0/450),     // 4.516
            Tuple.Create(MapLevel.L5, -2560.0/450),     // 5.689, native scale of L5 maps
            Tuple.Create(MapLevel.L5, -3225.0/450),     // 7.167
            Tuple.Create(MapLevel.L5, -4063.0/450)      // 9.029
        };

        LatLongCoord center = new LatLongCoord();
        double yScale;

        Dictionary<MapKey, TpqMap> maps = new Dictionary<MapKey, TpqMap>();

        public void Initalize()
        {
            // TODO: Load center and scale from settings
            center.longitude = -121.6875 * 3600;
            center.latitude = 46.8125 * 3600;
            yScale = -2032.0 / 450;
        }

        private MapLevel GetLevelFromScale(double yScale)
        {
            foreach(Tuple<MapLevel, double> scale in PresetYScales)
            {
                if (yScale == scale.Item2)
                    return scale.Item1;
            }
            throw new Exception("Scale does not exist in preset scales.");
        }

        public void ZoomIn()
        {
            for (int i = 0; i < PresetYScales.Length-1; i++)
            {
                if (yScale == PresetYScales[i].Item2)
                {
                    yScale = PresetYScales[i + 1].Item2;
                    return;
                }
            }
        }

        public void ZoomOut()
        {
            for (int i = 1; i < PresetYScales.Length; i++)
            {
                if (yScale == PresetYScales[i].Item2)
                {
                    yScale = PresetYScales[i -1].Item2;
                    return;
                }
            }
        }

        /// <summary>
        /// Returns the size of the map in seconds. This is based on the assumption all maps of the same level
        /// have the same size. If this ever changes, the logic needs to be changed too.
        /// </summary>
        /// <param name="level"></param>
        /// <returns></returns>
        Size<double> GetMapSizeWorld(MapLevel level)
        {
            Size<double> mapSize; // Map size in world coordinate

            switch (level)
            {
                case (MapLevel.L3):
                    mapSize.height = 60 * 60;
                    mapSize.width = 60 * 60;
                    break;
                case (MapLevel.L4):
                    mapSize.height = 30 * 60;
                    mapSize.width = 60 * 60;
                    break;
                case (MapLevel.L5):
                    mapSize.height = 7.5 * 60;
                    mapSize.width = 7.5 * 60;
                    break;
                default:
                    throw new NotImplementedException();
            }

            return mapSize;
        }

        /// <summary>
        /// Given a point and a map level, find out the map of that level that contains that point.
        /// </summary>
        /// <param name="coord">Coordinate of the point</param>
        /// <param name="level">Level of the map</param>
        /// <returns></returns>
        MapKey GetMapKey(LatLongCoord coord, MapLevel level)
        {
            Size<double> mapSize = GetMapSizeWorld(level);

            MapKey result;
            result.level = level;
            result.east = (int)(Math.Ceiling(coord.longitude / mapSize.width) * mapSize.width);
            result.south = (int)(Math.Floor(coord.latitude / mapSize.height) * mapSize.height);

            return result;
        }

        /// <summary>
        /// Get the path of the tpq file based on the map key.
        /// </summary>
        /// <param name="key">Map key</param>
        /// <returns></returns>
        public string GetFilePath(MapKey key)
        {
            int latitudeDegree = key.south / 3600;
            int longitudeDegree = -key.east / 3600;
            char latitudeBlock = (char)((int)'A' + (key.south % 3600) / 450);
            int longitudeBlock = 1 + (-key.east % 3600) / 450;

            string prefix = string.Empty;
            if (key.level == MapLevel.L5)
                prefix = "Q";
            else if (key.level == MapLevel.L4)
                prefix = "K";
            else if (key.level == MapLevel.L3)
                prefix = "G";
            // TODO: level 2 and 1

            string baseFileName = prefix + latitudeDegree.ToString() + longitudeDegree.ToString() + latitudeBlock.ToString() + longitudeBlock.ToString();
            return "ms-appx:///" + baseFileName + ".TPQ";
        }

        /// <summary>
        /// Load a map into memory.
        /// </summary>
        /// <param name="key">Map key</param>
        /// <returns></returns>
        public async Task<TpqMap> LoadMap(MapKey key)
        {
            TpqMap map;
            if (!maps.TryGetValue(key, out map))
            {
                map = new TpqMap();
                try
                {
                    await map.LoadFile(GetFilePath(key));
                }
                catch (Exception)
                {
                    // If the file fails to load, add an null entry to the map table anyway, to prevent further attempt to load the file.
                    map = null;
                }
                maps.Add(key, map);
            }
            return map;
        }

        /// <summary>
        /// Render the map on a bitmap
        /// </summary>
        /// <param name="center">Coordinate of the center of the rendering area</param>
        /// <param name="width">Width of the bitmap, in pixels</param>
        /// <param name="height">Height of the bitmap, in pixels</param>
        /// <param name="yScale">Pixels of second in the y direction</param>
        /// <returns></returns>
        public async Task<WriteableBitmap> Render(int width, int height)
        {
            // TODO: Implement caching mechanism.
            bool loadCacheOnly = false;

            MapLevel level = GetLevelFromScale(yScale);
            Size<double> mapSize = GetMapSizeWorld(level);
            MapKey key = GetMapKey(center, level);

            // Load map for the center so that we can calculate xScale.
            TpqMap centerMap = await LoadMap(key);

            // Calculate scale on x direction based on the y scale and the scale of the map. However, if the map doesn't exist, we still need
            // the scale. In this case, we calculate based on the cosine of the laitude at the center.
            double xScale = (centerMap != null) ?
                yScale / centerMap.pixelPerSecond.Item2 * centerMap.pixelPerSecond.Item1 :
                yScale * Math.Cos(center.latitude / 180.0 * Math.PI);

            SRect drawAreaScreen;
            drawAreaScreen.left = 0;
            drawAreaScreen.right = width;
            drawAreaScreen.top = 0;
            drawAreaScreen.bottom = height;

            GRect drawAreaWorld;
            drawAreaWorld.north = center.latitude - height / yScale / 2;
            drawAreaWorld.west = center.longitude - width / xScale / 2;
            drawAreaWorld.south = center.latitude + height / yScale / 2;
            drawAreaWorld.east = center.longitude + width / xScale / 2;

            Transformation worldToScreen;
            worldToScreen.yScale = yScale;
            worldToScreen.xScale = xScale;
            worldToScreen.xDelta = (drawAreaScreen.left * drawAreaWorld.east - drawAreaScreen.right * drawAreaWorld.west) / (drawAreaWorld.east - drawAreaWorld.west);
            worldToScreen.yDelta = (drawAreaScreen.bottom * drawAreaWorld.north - drawAreaScreen.top * drawAreaWorld.south) / (drawAreaWorld.north - drawAreaWorld.south);

            int minCol = (int)Math.Floor(drawAreaWorld.west / mapSize.width);
            int maxCol = (int)Math.Ceiling(drawAreaWorld.east / mapSize.width) - 1;
            int maxRow = (int)Math.Ceiling(drawAreaWorld.north / mapSize.height) - 1;
            int minRow = (int)Math.Floor(drawAreaWorld.south / mapSize.height);

            WriteableBitmap result = BitmapFactory.New(width, height);

            for (int row = minRow; row <= maxRow; row++)
                for (int col = minCol; col <= maxCol; col++)
                {
                    GRect thisBlock;

                    thisBlock.east = (col + 1) * mapSize.width;
                    thisBlock.west = col * mapSize.width;
                    thisBlock.south = row * mapSize.height;
                    thisBlock.north = (row + 1) * mapSize.height;

                    MapKey mapKey = GetMapKey(new LatLongCoord(thisBlock.south, thisBlock.east), level);
                    TpqMap map = await LoadMap(mapKey);

                    GRect clippedBlock = drawAreaWorld & thisBlock;
                    if (map == null)
                    {
                        if (!loadCacheOnly)
                        {
                            SRect screen = worldToScreen.Transform(clippedBlock);

                            result.FillRectangle(screen.left,
                                screen.top,
                                screen.right,
                                screen.bottom,
                                Colors.Magenta);
                        }
                    }
                    else
                    {
                        map.Draw(result, clippedBlock, worldToScreen, loadCacheOnly);
                    }
                }
            return result;
        }
    }
}
